package com.rmi;

import com.google.gson.Gson;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.InetAddress;
import java.net.Socket;
import java.net.SocketException;
import javassist.util.proxy.MethodHandler;

/**
 * @author Dmitry
 */
public class RmiMethodHandler implements MethodHandler {

    private final InetAddress address;
    private final int port;
    private final Gson gson;
    private final Class clazz;
    private final String id;
    private Socket socket;

    RmiMethodHandler(Class clazz, String id, InetAddress address, int port) {
        this.clazz = clazz;
        this.id = id;
        this.address = address;
        this.port = port;
        gson = new Gson();
    }

    @Override
    public Object invoke(Object self, Method thisMethod, Method proceed, Object[] args) throws Throwable {
        RmiHeader header = new RmiHeader();
        RmiHeader.RmiMethod rmiMethod = new RmiHeader.RmiMethod();
        if (id != null) {
            rmiMethod.setClassName(clazz.getName() + id);
        } else {
            rmiMethod.setClassName(clazz.getName());
        }

        rmiMethod.setMethodName(thisMethod.getName());
        RmiHeader.RmiParameter[] rmiParameters = new RmiHeader.RmiParameter[args.length];
        StringBuilder parameterData = new StringBuilder();
        Class[] parameterTypes = thisMethod.getParameterTypes();
        for (int i = 0; i < args.length; i++) {
            int offset = parameterData.length();
            parameterData.append(gson.toJson(args[i]));
            rmiParameters[i] = new RmiHeader.RmiParameter(parameterTypes[i].getName(), offset);
        }

        rmiMethod.setParameters(rmiParameters);
        header.setRmiMethod(rmiMethod);
        String outputData = gson.toJson(header) + "|" + parameterData.toString();
        String resultString = null;
        Socket tSocket = getSocket();
        try {
            OutputStream outputStream = new BufferedOutputStream(tSocket.getOutputStream());
            RmiStream rmiStream = new RmiStream();
            rmiStream.writeString(outputStream, outputData);
            InputStream resultStream = new BufferedInputStream(tSocket.getInputStream());
            resultString = rmiStream.toReadString(resultStream);
        } catch (SocketException exception) {
            throw new RuntimeException(exception);
        }

        return processResponse(resultString, thisMethod);
    }

    private Socket createSocket() {
        try {
            Socket tSocket = new Socket(address, port);
            tSocket.setKeepAlive(true);
            tSocket.setSendBufferSize(4096);
            tSocket.setReceiveBufferSize(4096);
            tSocket.setTcpNoDelay(true);
            return tSocket;
        } catch (IOException ex) {
            throw new RuntimeException(ex);
        }
    }

    public void closeSocket() {
        try {
            if (socket != null) {
                socket.close();
            }
        } catch (IOException ex) {
            throw new RuntimeException(ex);
        }
    }

    private Socket getSocket() {
        if (socket == null) {
            socket = createSocket();
        } else {
            closeSocket();
            socket = createSocket();
        }
        //} else if (socket.isClosed()) {
        //    socket = createSocket();
        //} else if (!socket.isConnected()) {
        //    closeSocket();
        //    socket = createSocket();
        //}
        return socket;
    }

    private Object processResponse(String result, Method method) throws Throwable {
        if (result == null) {
            return null;
        }

        int index = result.indexOf('|');
        String type = result.substring(0, index);
        String data = result.substring(index + 1);
        switch (type) {
            case "data":
                Class returnType = method.getReturnType();//method.getGenericReturnType()data.charAt(0)
                if (returnType == Void.TYPE || returnType == Void.class) {
                    return null;
                }
                return gson.fromJson(data, method.getGenericReturnType());
            case "exception":
                RmiHeader.RmiException rmiException = gson.fromJson(data, RmiHeader.RmiException.class);
                Throwable exception = null;
                try {
                    Class tClazz = Class.forName(rmiException.getClassName());
                    Constructor<Throwable> construction = tClazz.getConstructor(String.class);
                    exception = construction.newInstance(rmiException.getMessage());
                } catch (InvocationTargetException | IllegalAccessException | InstantiationException | ClassNotFoundException | NoSuchMethodException | SecurityException ex) {
                    throw new RuntimeException(ex);
                }

                throw exception;
            default:
                throw new RuntimeException("Unrecognized type - " + type);
        }
    }
}
